04.3 精通自定义 View 之属性动画进阶——为 ViewGroup 内的组件添加动画

返回自定义 View 目录

4.3.0 概述

为 ViewGroup 内的组件添加动画,Android 共提供了 4 种方法。
1. layoutAnimation 标签与 LayoutAnimationController
第一:定义一个 layoutAnimation 的 animation 文件,如:(anim/layout_animation.xml)

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:delay="1"
android:animationOrder="normal"
android:animation="@anim/slide_in_left"/>

第二步:在 viewGroup 类型的控件中,添加 android:layoutAnimation=”@anim/layout_animation”,如:

1
2
3
4
5
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutAnimation="@anim/layout_animation" />

其中 @anim/slide_in_left 文件:

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="1000">
<translate android:fromXDelta="-50%p" android:toXDelta="0"/>
<alpha android:fromAlpha="0.0" android:toAlpha="1.0"/>
</set>

注意:android:layoutAnimation 只在 ViewGroup 创建的时候,才会对其中的 item 添加动画。在创建成功以后,再向其中添加 item 将不会再有动画。

2. gridLayoutAnimation 标签与 GridLayoutAnimationController
gridLayoutAnimation 与 layoutAnimation 一样有缺陷:即在 GridView 初次创建的时候有入场动画,之后新添加的数据是不会有入场动画的。

详情请点击前往:layoutAnimation & gridLayoutAnimation

3. animateLayoutChanges 属性
在 API 11 之后,Android 为了支持 ViewGroup 类控件,在添加和移除其中控件时自动添加动画,为我们提供了一个非常简单的属性:android:animateLayoutChanges=[true/false],所有派生自 ViewGroup 的控件都具有此属性,只要在 XML 中添加上这个属性,就能实现添加/删除其中控件时,带有默认动画了。

4. LayoutTransition
在 API 11 之后引入,可以实现在 ViewGroup 动态添加或删除其中的控件时指定动画。动画可以自定义。对比前三种方法,LayoutTransition 是最强大的。

4.3.1 animateLayoutChanges 属性

在相应的 GroupView 子类中添加 android:animateLayoutChanges=”true”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/add_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加控件"/>
<Button
android:id="@+id/remove_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="移除控件"/>
</LinearLayout>
<LinearLayout
android:id="@+id/layoutTransitionGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical"/>
</LinearLayout>

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private LinearLayout layoutTransitionGroup;
private int i = 0;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
layoutTransitionGroup = findViewById(R.id.layoutTransitionGroup);
findViewById(R.id.add_btn).setOnClickListener(this);
findViewById(R.id.remove_btn).setOnClickListener(this);
}
private void addButtonView() {
i++;
Button button = new Button(this);
button.setText("button" + i);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
button.setLayoutParams(params);
layoutTransitionGroup.addView(button, 0);
}
private void removeButtonView() {
if (i > 0) {
layoutTransitionGroup.removeViewAt(0);
}
i--;
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.add_btn) {
addButtonView();
}
if (v.getId() == R.id.remove_btn) {
removeButtonView();
}
}
}

4.3.2 LayoutTransition

上面虽然在 ViewGroup 类控件 XML 中仅添加一行 android:animateLayoutChanges=[true] 即可实现内部控件添加删除时都加上动画效果。但却只能使用默认动画效果,而无法自定义动画。

为了能让我们自定义动画,谷歌在 API 11 时,同时为我们引入了一个类 LayoutTransaction。要使用LayoutTransaction是非常容易的,只需要三步:

1
2
3
4
5
6
7
// 1. 创建实例
LayoutTransaction transitioner = new LayoutTransition();
// 2. 创建动画并设置
ObjectAnimator animOut = ObjectAnimator.ofFloat(null, "rotation", 0f, 90f, 0f);
transitioner.setAnimator(LayoutTransition.DISAPPEARING, animOut);
// 3. 将 LayoutTransaction 设置进 ViewGroup
linearLayout.setLayoutTransition(mTransitioner);

在第二步中,transitioner.setAnimator 设置动画的函数声明为:

1
public void setAnimator(int transitionType, Animator animator)

  • int transitionType:表示当前应用动画的对象范围,取值有:
    APPEARING:元素在容器中出现时所定义的动画。
    DISAPPEARING:元素在容器中消失时所定义的动画。
    CHANGE_APPEARING:由于容器中要显现一个新的元素,其它需要变化的元素所应用的动画。
    CHANGE_DISAPPEARING:当容器中某个元素消失,其它需要变化的元素所应用的动画。
  • Animator animator:表示当前所选范围的控件所使用的动画。

1. APPEARING 与 DISAPPEARING

LayoutTransition.APPEARING 所对应的当一个控件出现时所对应的动画;LayoutTransition.DISAPPEARING 在一个控件被移除时所对应的动画。修改上一个示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
layoutTransitionGroup = findViewById(R.id.layoutTransitionGroup);
findViewById(R.id.add_btn).setOnClickListener(this);
findViewById(R.id.remove_btn).setOnClickListener(this);
mTransitioner = new LayoutTransition();
// 入场动画:view 在这个容器中出现时触发的动画
ObjectAnimator animIn = ObjectAnimator.ofFloat(null, "rotationY", 0f, 360f,0f);
mTransitioner.setAnimator(LayoutTransition.APPEARING, animIn);
// 出场动画:view 在这个容器中消失时触发的动画
ObjectAnimator animOut = ObjectAnimator.ofFloat(null, "rotation", 0f, 90f, 0f);
mTransitioner.setAnimator(LayoutTransition.DISAPPEARING, animOut);
layoutTransitionGroup.setLayoutTransition(mTransitioner);

同时要删除 XML 中的 android:animateLayoutChanges=”true” 设置。

2. CHANGE_APPEARING

在添加控件时,除了被添加控件本身的入场动画以外,其它需要移动位置的控件,在移动位置时,也被添加上了动画(left 点位移动画),这些除了被添加控件以外的其它需要移动位置的控件组合,所对应的动画就是 LayoutTransition.CHANGE_APPEARING。同样,在移除一个控件时,其它所有需要改变位置的控件组合所对应的动画就是 LayoutTransition.CHANGE_DISAPPEARING。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
layoutTransitionGroup = (LinearLayout) findViewById(R.id.layoutTransitionGroup);
findViewById(R.id.add_btn).setOnClickListener(this);
findViewById(R.id.remove_btn).setOnClickListener(this);
mTransitioner = new LayoutTransition();
// 入场动画:view 在这个容器中出现时触发的动画
ObjectAnimator animIn = ObjectAnimator.ofFloat(null, "rotationY", 0f, 360f,0f);
mTransitioner.setAnimator(LayoutTransition.APPEARING, animIn);
// 出场动画:view 在这个容器中消失时触发的动画
ObjectAnimator animOut = ObjectAnimator.ofFloat(null, "rotation", 0f, 90f, 0f);
mTransitioner.setAnimator(LayoutTransition.DISAPPEARING, animOut);
PropertyValuesHolder pvhLeft = PropertyValuesHolder.ofInt("left",0,100,0);
PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("top",1,1);
Animator changeAppearAnimator = ObjectAnimator.ofPropertyValuesHolder(
layoutTransitionGroup, pvhLeft,pvhBottom,pvhTop,pvhRight);
mTransitioner.setAnimator(LayoutTransition.CHANGE_APPEARING,changeAppearAnimator);
layoutTransitionGroup.setLayoutTransition(mTransitioner);
}

注意:
1、LayoutTransition.CHANGE_APPEARING 和 LayoutTransition.CHANGE_DISAPPEARING 必须使用 PropertyValuesHolder 所构造的动画才会有效果,不然无效!也就是说使用 ObjectAnimator 构造的动画,在这里是不会有效果的!
2、在构造 PropertyValuesHolder 动画时,“left”、“top” 属性的变动是必写的。如果不需要变动,则直接写为:

1
2
PropertyValuesHolder pvhLeft = PropertyValuesHolder.ofInt("left",0,0);
PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("top",0,0);

3、在构造 PropertyValuesHolder 时,所使用的 ofInt、ofFloat 中的参数值,第一个值和最后一个值必须相同,不然此属性所对应的的动画将被放弃,在此属性值上将不会有效果。
4、在构造 PropertyValuesHolder 时,所使用的 ofInt,ofFloat 中,如果所有参数值都相同,也将不会有动画效果。

3. CHANGE_DISAPPEARING

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PropertyValuesHolder outLeft = PropertyValuesHolder.ofInt("left",0,0);
PropertyValuesHolder outTop = PropertyValuesHolder.ofInt("top",0,0);
Keyframe frame0 = Keyframe.ofFloat(0f, 0);
Keyframe frame1 = Keyframe.ofFloat(0.1f, -20f);
Keyframe frame2 = Keyframe.ofFloat(0.2f, 20f);
Keyframe frame3 = Keyframe.ofFloat(0.3f, -20f);
Keyframe frame4 = Keyframe.ofFloat(0.4f, 20f);
Keyframe frame5 = Keyframe.ofFloat(0.5f, -20f);
Keyframe frame6 = Keyframe.ofFloat(0.6f, 20f);
Keyframe frame7 = Keyframe.ofFloat(0.7f, -20f);
Keyframe frame8 = Keyframe.ofFloat(0.8f, 20f);
Keyframe frame9 = Keyframe.ofFloat(0.9f, -20f);
Keyframe frame10 = Keyframe.ofFloat(1, 0);
PropertyValuesHolder mPropertyValuesHolder = PropertyValuesHolder.ofKeyframe(
"rotation",frame0,frame1,frame2,frame3,frame4,
frame5,frame6,frame7,frame8,frame9,frame10);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
this, outLeft, outTop, mPropertyValuesHolder);
mTransitioner.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, animator);

第一步:由于 left、top 属性是必须的,但我们做响铃效果时,是不需要 left,top 变动的,所有给他们设置为无效值。
第二步:用 KeyFrame 构造 PropertyValuesHolder。
第三步:设置 LayoutTransition.CHANGE_DISAPPEARING 动画。

4.3.3 其他函数

1. 基本设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 设置所有动画完成所需要的时长
*/
public void setDuration(long duration)
/**
* 针对单个type,设置动画时长;
* transitionType取值为:APPEARING、DISAPPEARING、CHANGE_APPEARING、CHANGE_DISAPPEARING
*/
public void setDuration(int transitionType, long duration)
/**
* 针对单个type设置插值器
* transitionType取值为:APPEARING、DISAPPEARING、CHANGE_APPEARING、CHANGE_DISAPPEARING
*/
public void setInterpolator(int transitionType, TimeInterpolator interpolator)
/**
* 针对单个type设置动画延时
* transitionType取值为:APPEARING、DISAPPEARING、CHANGE_APPEARING、CHANGE_DISAPPEARING
*/
public void setStartDelay(int transitionType, long delay)
/**
* 针对单个type设置,每个子item动画的时间间隔
*/
public void setStagger(int transitionType, long duration)

2. LayoutTransition 设置监听

1
2
3
4
5
public void addTransitionListener(TransitionListener listener)
public interface TransitionListener {
public void startTransition(LayoutTransition transition, ViewGroup container,View view, int transitionType);
public void endTransition(LayoutTransition transition, ViewGroup container,View view, int transitionType);
}

在 TransitionListener 中总共有四个参数:

  • LayoutTransition transition:当前的 LayoutTransition 实例。
  • ViewGroup container:当前应用 LayoutTransition 的 container。
  • View view:当前在做动画的 View 对象。
  • int transitionType:当前的 LayoutTransition 类型,取值有:APPEARING、DISAPPEARING、CHANGE_APPEARING、CHANGE_DISAPPEARING。

在添加控件时,先是 start 回调,再是 end 回调;APPEARING 事件所对应的 View 是控件,而 CHANGE_APPEARING 所对应的控件是容器。删除控件时,原理相同。

这是因为,在添加控件时,APPEARING 事件只针对当前被添加的控件做动画,所以返回的 View 是当前被添加的控件。而 CHANGE_APPEARING 是对容器中所有已存在的控件做动画,所以返回的 View 是容器。